Type Game

	Const NET_TWEEN#                = 0.25
	
	Const NET_WARP_THRESHOLD#       = 100.0 * 100.0
	Const NET_ANIM_WARP_THRESHOLD#  = 2.0
	Const NET_ANGLE_WARP_THRESHOLD# = 45.0
	
	Global entityList:TList = New TList
	
	Global saveName$
	Global saves:TList = New TList
	
	Const PLAYER_NAME_LENGTH% = 20
	
	Global playerName$
	
	Global entities:NetEntity[65536]
	Global lastEntityID%
	
	Global typeMap:TMap = CreateMap()
	Global types:TTypeId[]
	
	Function Init%()
	
		saveName$ = ""
		
		Local saveDir% = ReadDir("saves")
		
		If Not saveDir 
		
			If Not CreateDir("Saves")
			
				Console.ConsolePrint("Error - Unable to to create game save directory")
				
				Return False
				
			EndIf
			
			saveDir = ReadDir("saves")
			
			If Not saveDir
			
				Console.ConsolePrint("Error - Unable to open game save directory")
			
				Return False
				
			EndIf
			
		EndIf
		
		Local fileName$ = NextFile$(saveDir)
		
		While fileName$ <> ""
			
			Local extension$ = Lower$(Right$(fileName$, 4))
			
			If extension$ = ".sav"
			
				Local save$ = Lower$(Left$(fileName$, Len(fileName$) - 4))
				
				saves.AddLast(save$)
				
			EndIf
			
			fileName$ = NextFile$(saveDir)
		
		Wend
		
		CloseDir(saveDir)
		
		entityList.Clear()
		Graphics.spriteEntities.Clear()
		
		For Local e% = 0 To 65536 - 1
		
			entities[e] = Null
			
		Next
		
		lastEntityID = 0
		
		Local typeIndex% = 0
		ClearMap(typeMap)
		
		Local netEntityType:TTypeId = TTypeId.ForName("NetEntity")
		
		For Local myTypeID:TTypeId = EachIn netEntityType.DerivedTypes()
					
			MapInsert(typeMap, myTypeID, String(typeIndex))
			typeIndex = typeIndex + 1
		
		Next
		
		types = TTypeId[](netEntityType.DerivedTypes().ToArray())
		
		Local gameEntityType:TTypeId = TTypeId.ForName("GameEntity")
		
		For Local myTypeID:TTypeId = EachIn gameEntityType.DerivedTypes()
			
			Local myEntity:GameEntity = GameEntity(myTypeID.NewObject())
			
			myEntity.Init()
			myEntity = Null
		
		Next
		
		Return True
		
	End Function
	
	Function Update%()
		
		For Local myEntity:GameEntity = EachIn entityList
		
			myEntity.Update()
	
		Next
		
		Return True
		
	End Function
	
	Function Shutdown%()
	
		saves.Clear()
	
		entityList.Clear()
		Graphics.spriteEntities.Clear()
		
		For Local e% = 0 To 65536 - 1
		
			entities[e] = Null
			
		Next
		
		ClearMap(typeMap)
	
		Return True
		
	End Function
		
	Function NewGame%()
		
		AddPlayer(0) ' setup initial game state
	
		Return True
		
	End Function
	
	Function EndGame%()
		
		For Local myEntity:GameEntity = EachIn entityList
			
			myEntity.Destroy()
			
		Next
		
		entityList.Clear()
		Graphics.spriteEntities.Clear()
		
		For Local e% = 0 To 65536 - 1
		
			entities[e] = Null
			
		Next
		
		lastEntityID = 0
		
		Return True
		
	End Function
		
	Function SaveGame%()
	
		If Not saves.Contains(saveName$)
			
			saves.AddLast(saveName$)
			
		EndIf
		
		Local saveFile:TStream = WriteFile("saves\" + saveName$ + ".sav")
		
		If Not saveFile
		
			Console.ConsolePrint("Error saving game '" + saveName$ + "'")
			
			Return False
			
		Else
		
			' todo: load save game
		
			CloseFile(saveFile)
			
		EndIf
		
		Return True
			
	End Function
	
	Function LoadGame%(Save$)
		
		If saves.Contains(save$)
		
			Local saveFile:TStream = ReadFile("saves\" + save$ + ".sav")
		
			If Not saveFile
			
				Console.ConsolePrint("Error loading game'" + save$ + "'")
				
				Return False
				
			EndIf
				
			' todo: load saved game
			
			CloseFile(saveFile)
			
		Else
			
			saveName$ = ""
			
			Return NewGame()
		
		EndIf
		
		saveName$ = save$
	
		Return True
		
	End Function
	
	Function AddPlayer(playerID%)
		
		'Local myPlayer:TestPlayer = New TestPlayer	' setup a player
		
		'myPlayer.playerID = playerID
		'myPlayer.Create()
			
	End Function
	
	Function RemovePlayer(playerID%)
	
		For Local searchPlayer:PlayerEntity = EachIn entityList
		
			If searchPlayer.playerID = playerID
			
				searchPlayer.Destroy()
				
			EndIf
			
		Next
	
	End Function
	
End Type

Type GameEntity

	Field cxPos# = 0.0, cyPos# = 0.0
	Field oxPos# = 0.0, oyPos# = 0.0
	
	Field cAngle# = 0.0
	Field oAngle# = 0.0
	
	Field scaleX# = 1.0, scaleY# = 1.0 ' used for drawing only
	Field alpha# = 1.0
	
	Field blendMode% = Graphics.DEFAULT_BLEND
	
	Field drawZ# = 0.0
		
	Field cAnimTime# = 0.0
	Field oAnimTime# = 0.0
	
	Field animIndex% = 0
	Field sprite:SpriteResource = Null
	
	Function Init()
	End Function
	
	Method Create()
	
		Game.entityList.AddLast(Self)
		Graphics.spriteEntities.AddLast(Self)
			
	End Method
	
	Method Destroy()
	
		Game.entityList.Remove(Self)
		Graphics.spriteEntities.Remove(Self)
			
	End Method
	
	Method Update()
	
		Local _cxPos# = cxPos#
		Local _cyPos# = cyPos#
		
		Local _cAngle# = cAngle#
		
		cxPos# = 2.0 * cxPos# - oxPos#
		cyPos# = 2.0 * cyPos# - oyPos#
		
		cAngle# = 2.0 * cAngle# - oAngle#
		
		oxPos# = _cxPos#
		oyPos# = _cyPos#
		
		oAngle# = _cAngle#
		
		If sprite
		
			If Len(sprite.animations)
			
				Local _cAnimTime# = cAnimTime#
				
				cAnimTime# = 2.0 * cAnimTime# - oAnimTime#
				
				oAnimTime# = _cAnimTime#
				
			EndIf
			
		EndIf

		' todo: collision detection
				
	End Method
	
	Method Draw()
		
		If sprite
		
			SetTransform(cAngle#, scaleX#, scaleY#)
			
			SetAlpha(alpha#)
			SetBlend(blendMode)
			
			If Len(sprite.animations)
			
				If animIndex < 0 Or animIndex >= Len(sprite.animations) Then animIndex = 0
				
				If cAnimTime# >= Len(sprite.animations[animIndex].frames) Then cAnimTime# = cAnimTime# - Floor(cAnimTime# / Len(sprite.animations[animIndex].frames)) * Len(sprite.animations[animIndex].frames) + sprite.animations[animIndex].loopFrame
				If cAnimTime# < 0 Then cAnimTime# = 0.0
					
				DrawImage(sprite.image, cxPos#, cyPos#, sprite.animations[animIndex].frames[Floor(cAnimTime#)])
				
			Else
			
				DrawImage(sprite.image, cxPos#, cyPos#)
			
			EndIf
			
		EndIf
		
	End Method
		
	Method SetVelocity(x_vel#, y_vel#)
	
		oxPos# = cxPos# - x_vel#
		oyPos# = cyPos# - y_vel#
		
	End Method
	
	Method GetVelocityX#()
	
		Return (cxPos# - oxPos#)
		
	End Method
	
	Method GetVelocityY#()
	
		Return (cyPos# - oyPos#)
		
	End Method
	
	Method GetAngleVelocity#()
	
		Return cAngle# - oAngle
		
	End Method
	
	Method SetAngleVelocity#(a_vel#)
	
		oAngle# = cAngle# - a_vel#
		
	End Method
	
	Method SetAngle(ang#, instant% = True)
		
		Local a_vel#
		
		If instant
		
			a_vel# = GetAngleVelocity#()
			
		EndIf
		
		cAngle# = ang#
		
		If instant
		
			SetAngleVelocity#(a_vel#)
			
		EndIf
	
	End Method
	
	Method GetAnimSpeed#()
	
		Return cAnimTime# - oAnimTime#
		
	End Method
	
	Method SetAnimSpeed(a_vel#)
	
		oAnimTime# = cAnimTime# - a_vel#
		
	End Method
	
	Method SetAnimTime(animTime#, instant% = True)
		
		Local a_vel#
		
		If instant
		
			a_vel# = GetAnimSpeed#()
			
		EndIf
		
		cAnimTime# = animTime#
		
		If instant
		
			SetAnimSpeed(a_vel#)
			
		EndIf
			
	End Method
	
	Method SetPosition(x_pos#, y_pos#, instant% = True)
		
		Local x_vel#
		Local y_vel#
		
		If instant
		
			x_vel# = GetVelocityX#()
			y_vel# = GetVelocityY#()
			
		EndIf
		
		cxPos# = x_pos#
		cyPos# = y_pos#
		
		If instant
		
			SetVelocity(x_vel#, y_vel#)
			
		EndIf

	End Method
	
End Type

Type NetEntity Extends GameEntity

	Field entityID%
	Field typeIndex%
	
	Field nxPos#
	Field nyPos#
	
	Field nAnimTime#
	Field nAngle#
	
	Method Create()
		
		Super.Create()
		
		If Network.netState = Network.STATE_SERVER
		
			HostCreate()
			
		Else If Network.netState = Network.STATE_CLIENT
		
			ClientCreate()
			
		EndIf
		
	End Method
	
	Method Destroy()
		
		Super.Destroy()
		
		If Network.netState = Network.STATE_SERVER
		
			HostDestroy()
			
		Else If Network.netState = Network.STATE_CLIENT
		
			ClientDestroy()
			
		EndIf
		
	End Method
	
	Method HostCreate()
		
		Local typeID:TTypeId = TTypeId.ForObject(Self)
		
		typeIndex = String(MapValueForKey(Game.typeMap, typeId)).ToInt()
		
		GetNewEntityID()
	
		For Local client:NetClient = EachIn Network.clientList
		
			client.sendPacket.stateEvent.entitiesCreated.AddLast(String(entityID))
		
		Next
			
	End Method 
	
	Method ClientCreate()
	
		Game.entities[entityID] = Self
		
		nxPos# = cxPos#
		nyPos# = cyPos#
		
		nAnimTime# = cAnimTime#
		nAngle#    = cAngle#
		
	End Method
	
	Method HostDestroy()
	
		For Local client:NetClient = EachIn Network.clientList
		
			client.sendPacket.stateEvent.entitiesDestroyed.AddLast(String(entityID))
		
		Next
		
		ReleaseEntityID()
		
	End Method 
	
	Method ClientDestroy()
		
		Game.entities[entityID] = Null
	
	End Method
		
	Method GetNewEntityID%()
	
		For Local i% = 0 To 65536 - 1
		
			If Game.entities[Game.lastEntityID] = Null Then Exit
		
			Game.lastEntityID = (Game.lastEntityID + 1) & $FFFF
		
		Next
		
		If Game.entities[Game.lastEntityID] <> Null
		
			Console.ConsolePrint("Error - Unable to allocate new entity ID")
			
			entityID = -1
			
			Return False
			
		EndIf
		
		Game.entities[Game.lastEntityID] = Self
		
		Game.lastEntityID = (Game.lastEntityID + 1) & $FFFF
		
		Return True
		
	End Method
	
	Method ReleaseEntityID()
	
		If entityID >= 0
		
			Game.entities[entityID] = Null
			entityID = -1
			
		EndIf
		
	End Method
		
	Method Update()
	
		Super.Update()
	
		If Network.netState = Network.STATE_SERVER
		
			HostUpdate()
			
		Else If Network.netState = Network.STATE_CLIENT
		
			ClientUpdate()
			
		EndIf
		
	End Method

	Method HostUpdate()
	End Method
	
	Method ClientUpdate()
	
		nxPos# = nxPos# * (Game.NET_TWEEN#) + cxPos# * (1.0 - Game.NET_TWEEN#)
		nyPos# = nyPos# * (Game.NET_TWEEN#) + cyPos# * (1.0 - Game.NET_TWEEN#)
		
		nAnimTime# = nAnimTime# * (Game.NET_TWEEN#) + cAnimTime# * (1.0 - Game.NET_TWEEN#)
		
		If Abs(nAngle# - cAngle#) > 180.0 Then nAngle# = nAngle# + (Floor((cAngle# - nAngle#) / 360.0) + 1.0) * 360.0
		
		nAngle# = nAngle# * (Game.NET_TWEEN#) + cAngle# * (1.0 - Game.NET_TWEEN#)
		
	End Method
	
	Method Draw()
		
		If Network.netState = Network.STATE_CLIENT
			
			ClientDraw()
			
		Else If Network.netState = Network.STATE_SERVER
		
			HostDraw()
			
		EndIf
			
	End Method
	
	Method HostDraw()
		
		Super.Draw()
		
	End Method
	
	Method ClientDraw()
	
		If sprite
		
			SetTransform(nAngle#, scaleX#, scaleY#)
			
			SetAlpha(alpha#)
			SetBlend(blendMode)
			
			If Len(sprite.animations)
			
				If animIndex < 0 Or animIndex >= Len(sprite.animations) Then animIndex = 0
				
				If cAnimTime# >= Len(sprite.animations[animIndex].frames) Then cAnimTime# = cAnimTime# - Floor(cAnimTime# / Len(sprite.animations[animIndex].frames)) * Len(sprite.animations[animIndex].frames) + sprite.animations[animIndex].loopFrame
				If cAnimTime# < 0 Then cAnimTime# = 0.0
				
				If nAnimTime# >= Len(sprite.animations[animIndex].frames) Then nAnimTime# = nAnimTime# - Floor(nAnimTime# / Len(sprite.animations[animIndex].frames)) * Len(sprite.animations[animIndex].frames) + sprite.animations[animIndex].loopFrame
				If nAnimTime# < 0 Then nAnimTime# = 0.0				
					
				DrawImage(sprite.image, nxPos#, nyPos#, sprite.animations[animIndex].frames[Floor(nAnimTime#)])
				
			Else
			
				DrawImage(sprite.image, nxPos#, nyPos#)
			
			EndIf
			
		EndIf
			
	End Method
	
	Method HostSerializeState(buffer:RAMStream)
	
		buffer.WritePackedFloat32(cxPos#)
		buffer.WritePackedFloat32(cyPos#)

		buffer.WritePackedFloat16(cAnimTime#)
		buffer.WritePackedFloat8(cAngle# / 360.0)
		
	End Method
		
	Method ClientDeserializeState(buffer:RAMStream)
	
		cxPos# = buffer.ReadPackedFloat32()
		cyPos# = buffer.ReadPackedFloat32()

		Local dx# = nxPos# - cxPos#
		Local dy# = nyPos# - cyPos#
		
		If (dx# * dx# + dy# * dy#) > Game.NET_WARP_THRESHOLD#
	
			nxPos# = cxPos#
			nyPos# = cyPos#
		
		EndIf
		
		cAnimTime# = buffer.ReadPackedFloat16#()
		cAngle#    = buffer.ReadPackedFloat8#()	* 360.0
		
		If Abs(nAnimTime# - cAnimTime#) > Game.NET_ANIM_WARP_THRESHOLD#
		
			nAnimTime# = cAnimTime#
			
		EndIf
		
		If Abs(nAngle# - cAngle#) > 180.0 Then nAngle# = nAngle# + (Floor((cAngle# - nAngle#) / 360.0) + 1.0) * 360.0
		
		If Abs(nAngle# - cAngle#) > Game.NET_ANGLE_WARP_THRESHOLD#
		
			nAngle# = cAngle#
			
		EndIf
		
	End Method
	 
End Type

Type PlayerEntity Extends NetEntity

	Field playerID% = 0
	
	Field xLook# = 0.0
	Field yLook# = 0.0
	
	Field nxLook# = 0.0
	Field nyLook# = 0.0
	
	Field oKeys:Byte = 0
	Field cKeys:Byte = 0
	
	Field dirty% = 0	' set when player position has been moved by server entities
	
	Method Control()
	End Method
	
	Method ProcessInput()
	End Method
	
	Method HostUpdate()
	
		ClientUpdate()
		
	End Method
	
	Method ClientUpdate()
		
		If playerID <> Network.playerID

			Super.ClientUpdate()
			
			nxLook# = nxLook# * (GAME.NET_TWEEN#) + xLook# * (1.0 - Game.NET_TWEEN#)
			nyLook# = nyLook# * (GAME.NET_TWEEN#) + yLook# * (1.0 - Game.NET_TWEEN#)
			
		Else
		
			Control()
			ProcessInput()
			
			nxLook# = xLook#
			nyLook# = yLook#
			
			nxPos# = cxPos#
			nyPos# = cyPos#
			
			nAngle# = cAngle#
			nAnimTime# = cAnimTime#
						
			Super.ClientUpdate()
			
		EndIf
		
	End Method				
	
	Method HostSerializeState(buffer:RAMStream)
	
		If dirty
		
			buffer.WriteByte(1)
			
			Super.HostSerializeState(buffer)
			
		Else
		
			buffer.WriteByte(0)
			
		EndIf
		
		buffer.WriteByte(playerID)

		dirty = 0
				
	End Method
		
	Method ClientDeserializeState(buffer:RAMStream)
	
		dirty = buffer.ReadByte()
		
		If dirty
		
			Super.ClientDeserializeState(buffer)
			
		EndIf

		playerID = buffer.ReadByte()
			
	End Method
	
	Method ClientSerializeControl(buffer:RAMStream)
		
		buffer.WritePackedFloat32(cxPos#)
		buffer.WritePackedFloat32(cyPos#)
		
		buffer.WritePackedFloat32(xLook#)
		buffer.WritePackedFloat32(yLook#)
		
		buffer.WriteByte(cKeys)
		
	End Method
	
	Method HostDeserializeControl(buffer:RAMStream, _playerID%)
			
		If playerID = _playerID
		
			cxPos# = buffer.ReadPackedFloat32#()
			cyPos# = buffer.ReadPackedFloat32#()
			
			xLook# = buffer.ReadPackedFloat32#()
			yLook# = buffer.ReadPackedFLoat32#()
			
			Local dx# = nxLook# - xLook#
			Local dy# = nyLook# - yLook#
			
			If (dx# * dx# + dy# * dy#) > Game.NET_WARP_THRESHOLD#
			
				nxLook# = xLook#
				nyLook# = yLook#
				
			EndIf
			
			dx# = cxPos# - nxPos#
			dy# = cyPos# - nyPos#
			
			If (dx# * dx# + dy# * dy#) > Game.NET_WARP_THRESHOLD#
			
				nxPos# = cxPos#
				nyPos# = cyPos#
				
			EndIf
						
			oKeys = cKeys
			cKeys = buffer.ReadByte()
			
			ProcessInput()
			
		Else
		
			buffer.ReadPackedFloat32#()
			buffer.ReadPackedFloat32#()
			
			buffer.ReadPackedFloat32#()
			buffer.ReadPackedFloat32#()
			
			buffer.ReadByte()
			
		EndIf
			
	End Method
	
End Type

Type TestPlayer Extends PlayerEntity
	

	

End Type

Type Explosion Extends GameEntity

	Global explosionSprite:SpriteResource

	Field life# = 36.0
			
	Function Init()
	
		explosionSprite = Graphics.GetSpriteResource("gfx\explosion.png")
		
	End Function
	
	Method Create()
		
		Super.Create()
		
		sprite = explosionSprite
		
		SetAnimSpeed(0.5)
		
		scaleX# = 2.0
		scaleY# = 2.0
		
		SetPosition(Rnd(-48.0, 48.0) + MouseX(), Rnd(-48.0, 48.0) + MouseY(), True)
		SetVelocity(Rnd(-0.5, 0.5), Rnd(-0.5, 0.5))
		
		SetAngleVelocity(Rnd(-0.5, 0.5))
		
	End Method
	
	Method Update()
	
		Super.Update()
		
		life# = life# - 1.0
		
		If life < 0.0
		
			Destroy()
			
		Else
		
			alpha# = life# / 35.0
			
		EndIf
		
	End Method
	
End Type